/* 
 * Copyright 2012 essendi it GmbH
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * 		http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package de.essendi.vaadin.ui.component.numberfield;

import static de.essendi.vaadin.ui.component.numberfield.widgetset.shared.Constants.*;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;

import com.vaadin.data.Validator;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.terminal.PaintException;
import com.vaadin.terminal.PaintTarget;
import com.vaadin.ui.TextField;

import de.essendi.vaadin.ui.component.numberfield.widgetset.client.ui.VNumberField;
import de.essendi.vaadin.ui.component.numberfield.widgetset.shared.NumberValidator;
import de.essendi.vaadin.ui.component.numberfield.widgetset.shared.NumberFieldAttributes;

/**
 * <p>
 * Provides a numeric field with automatic keystroke filtering and validation
 * for integer (123) and decimal numbers (12.3). The minus sign and
 * user-definable grouping and decimal separators are supported.
 * </p>
 * <p>
 * Inputs are validated on client- <b>and</b> server-side. The client-side
 * validator gets active on every keypress in the field. If the keypress would
 * lead to an invalid value, it is suppressed and the value remains unchanged.
 * The server-side validation is triggered when the field loses focus, see
 * {@link #addServerSideValidator()} for more details. To omit server-side
 * validation (which is enabled per default), you can call
 * {@link #removeServerSideValidator()}.
 * </p>
 * <p>
 * A user-entered value is formatted automatically when the field's focus is
 * lost. <code>NumberField</code> uses {@link DecimalFormat} for formatting and
 * send the formatted value of the input back to client. There's a number of
 * setters to define the format, see the code example below for a general view.
 * </p>
 * <p>
 * Some features in an usage example: <blockquote>
 * 
 * <pre>
 * NumberField numField = new NumberField(); // NumberField extends TextField
 * numField.setDecimalAllowed(true); // not just integers (by default, decimals are allowed)
 * numField.setDecimalPrecision(2); // maximum 2 digits after the decimal separator
 * numField.setDecimalSeparator(','); // e.g. 1,5
 * numField.setDecimalSeparatorAlwaysShown(true); // e.g. 12345 -&gt; 12345,
 * numField.setMinimumFractionDigits(2); // e.g. 123,4 -&gt; 123,40
 * numField.setGroupingUsed(true); // use grouping (e.g. 12345 -&gt; 12.345)
 * numField.setGroupingSeparator('.'); // use '.' as grouping separator
 * numField.setGroupingSize(3); // 3 digits between grouping separators: 12.345.678
 * numField.setMinValue(0); // valid values must be &gt;= 0 ...
 * numField.setMaxValue(999.9); // ... and &lt;= 999.9
 * numField.setErrorText(&quot;Invalid number format!&quot;); // feedback message on bad input
 * numField.setNegativeAllowed(false); // prevent negative numbers (defaults to true)
 * numField.setValueIgnoreReadOnly(&quot;10&quot;); // set the field's value, regardless whether it is read-only or not
 * numField.removeValidator(); // omit server-side validation
 * </pre>
 * 
 * </blockquote>
 * </p>
 */
@SuppressWarnings("serial")
@com.vaadin.ui.ClientWidget(VNumberField.class)
public class NumberField extends TextField {

	// Server-side validator
	private Validator numberValidator;

	// All NumberField instances share the same formatter
	private static DecimalFormat decimalFormat = new DecimalFormat();

	// Settings needed both on server- and client-side
	private NumberFieldAttributes attributes = new NumberFieldAttributes();

	// Settings needed only on server-side and therefore not part of
	// NumberFieldAttributes
	private String errorText = "Invalid number";
	private int groupingSize;
	private int minimumFractionDigits;
	private boolean decimalSeparatorAlwaysShown;

	/**
	 * <p>
	 * Constructs an empty <code>NumberField</code> with no caption. The field
	 * is bound to a server-side validator (see
	 * {@link #addServerSideValidator()}) and immediate mode is set to true;
	 * this is necessary for the validation of the field to occur immediately
	 * when the input focus changes.
	 * </p>
	 * <p>
	 * The decimal/grouping separator defaults to the user's local
	 * decimal/grouping separator. Grouping (e.g. 12345 -> 12.345) is enabled,
	 * the maximum precision to display after the decimal separator is set to 2
	 * per default.
	 * </p>
	 */
	public NumberField() {
		setImmediate(true);
		setDefaultValues();
	}

	private void setDefaultValues() {
		setGroupingUsed(true);
		groupingSize = 3;
		setDecimalPrecision(2);
		minimumFractionDigits = 0;
		decimalSeparatorAlwaysShown = false;
		addServerSideValidator();

		// Set the decimal/grouping separator to the user's default locale
		attributes.setDecimalSeparator(DecimalFormatSymbols.getInstance()
				.getDecimalSeparator());
		attributes.setGroupingSeparator(DecimalFormatSymbols.getInstance()
				.getGroupingSeparator());

		// Member "attributes" defines some defaults as well!
	}

	private void createNumberValidator() {
		// Create our server-side validator
		numberValidator = new Validator() {

			public boolean isValid(Object value) {
				return validateValue(value);
			}

			public void validate(Object value) throws InvalidValueException {
				if (!isValid(value)) {
					throw new InvalidValueException(errorText);
				}
			}

		};
	}

	private boolean validateValue(Object value) {
		if (value == null || !(value instanceof String)) {
			return false;
		}

		if ("".equals(value)) {
			return true;
		}

		final boolean isValid;
		if (isDecimalAllowed()) {
			isValid = NumberValidator
					.isValidDecimal((String) value, attributes);
		} else {
			isValid = NumberValidator
					.isValidInteger((String) value, attributes);
		}

		return isValid;
	}

	/**
	 * Validates the field's value according to the validation rules determined
	 * with the setters of this class (e.g. {@link #setDecimalAllowed(boolean)}
	 * or {@link #setMaxValue(double)}).
	 * 
	 * @return True, if validation succeeds; false, if validation fails.
	 */
	public boolean isValid() {
		return validateValue(getValue());
	}

	@Override
	public void paintContent(PaintTarget target) throws PaintException {
		super.paintContent(target);

		// Paint any component specific content by setting attributes.
		// These attributes can be read in the widget's updateFromUIDL().
		// paintContent() is called when the contents of the component should be
		// painted in response to the component first being shown or having been
		// altered so that its visual representation is changed.
		target.addAttribute(ATTRIBUTE_ALLOW_DECIMALS, isDecimalAllowed());
		target.addAttribute(ATTRIBUTE_ALLOW_NEGATIVES, isNegativeAllowed());
		target.addAttribute(ATTRIBUTE_DECIMAL_PRECISION, getDecimalPrecision());
		target.addAttribute(ATTRIBUTE_DECIMAL_SEPARATOR,
				(int) getDecimalSeparator());
		target.addAttribute(ATTRIBUTE_USE_GROUPING, isGroupingUsed());
		target.addAttribute(ATTRIBUTE_GROUPING_SEPARATOR,
				(int) getGroupingSeparator());
		target.addAttribute(ATTRIBUTE_MIN_VALUE, getMinValue());
		target.addAttribute(ATTRIBUTE_MAX_VALUE, getMaxValue());

		// Use DecimalFormat to format the user-input and send the
		// formatted value back to client.
		target.addAttribute(ATTRIBUTE_SERVER_FORMATTED_VALUE,
				getValueAsFormattedDecimalNumber());
	}

	private String getValueAsFormattedDecimalNumber() {
		String value = (String) getValue();
		if (value == null || "".equals(value)) {
			return value;
		}

		try {
			synchronized (decimalFormat) {
				setDecimalFormatToNumberFieldAttributes();
				Number valueAsNumber = decimalFormat.parse(value);
				return decimalFormat.format(valueAsNumber);
			}
		} catch (Exception e) {
			e.printStackTrace();
			return value;
		}
	}

	private void setDecimalFormatToNumberFieldAttributes() {
		synchronized (decimalFormat) {
			decimalFormat.setGroupingUsed(attributes.isGroupingUsed());
			decimalFormat.setGroupingSize(groupingSize);
			decimalFormat.setMinimumFractionDigits(minimumFractionDigits);
			decimalFormat
					.setDecimalSeparatorAlwaysShown(decimalSeparatorAlwaysShown);

			// Adapt decimal format symbols
			DecimalFormatSymbols symbols = decimalFormat
					.getDecimalFormatSymbols();
			symbols.setDecimalSeparator(attributes.getDecimalSeparator());
			symbols.setGroupingSeparator(attributes.getGroupingSeparator());
			decimalFormat.setDecimalFormatSymbols(symbols);
		}
	}

	/**
	 * <p>
	 * Binds the field to a server-side validator. The validation (is it a valid
	 * integer/decimal number?) provides feedback about bad input. If the input
	 * is recognized as invalid, an {@link InvalidValueException} is thrown,
	 * which reports an error message and mark the field as invalid. The error
	 * message can be set with {@link #setErrorText()}.
	 * </p>
	 * <p>
	 * To have the validation done immediately when the field loses focus,
	 * immediate mode should not be disabled (state is enabled per default).
	 * </p>
	 */
	public void addServerSideValidator() {
		if (numberValidator == null) {
			createNumberValidator();
		}
		super.addValidator(numberValidator);
	}

	/**
	 * Removes the field's validator.
	 * 
	 * @see #addServerSideValidator()
	 */
	public void removeServerSideValidator() {
		super.removeValidator(numberValidator);
	}

	/**
	 * @param newValue
	 *            Must be a string!
	 */
	@Override
	public void setValue(Object newValue) throws IllegalArgumentException,
			ReadOnlyException, ConversionException {
		if (!(newValue instanceof String)) {
			throw new IllegalArgumentException(
					"NumberField#setValue() expects a String as argument.");
		}
		super.setValue(newValue);
	}

	/**
	 * @param newValue
	 *            Sets the value of the field to a double.
	 */
	public void setValue(Double newValue) throws ReadOnlyException,
			ConversionException {
		// Use BigDecimal to avoid scientific notation and thus allow
		// NumberValidator to work properly
		String noExponent = BigDecimal.valueOf(newValue).toPlainString();

		String localizedValue = replacePointWithDecimalSeparator(noExponent);
		super.setValue(localizedValue);
	}

	/**
	 * Sets the value of the field, regardless whether the field is in read-only
	 * mode.
	 * 
	 * @param newValue
	 *            The field's new String value.
	 */
	public void setValueIgnoreReadOnly(String newValue) {
		boolean wasReadOnly = isReadOnly();
		if (isReadOnly()) {
			setReadOnly(false);
		}
		setValue(newValue);
		if (wasReadOnly) {
			setReadOnly(true);
		}
	}

	/**
	 * Sets the value of the field, regardless whether the field is in read-only
	 * mode.
	 * 
	 * @param newValue
	 *            The field's new Double value.
	 */
	public void setValueIgnoreReadOnly(Double newValue) {
		boolean wasReadOnly = isReadOnly();
		if (isReadOnly()) {
			setReadOnly(false);
		}
		setValue(newValue);
		if (wasReadOnly) {
			setReadOnly(true);
		}
	}

	/**
	 * See {@link NumberFieldAttributes#setDecimalAllowed(boolean)}.
	 * <p>
	 * As a side-effect, the minimum number of digits allowed in the fraction
	 * portion of a number is set to 0 if decimalAllowed is false (so that
	 * {@link DecimalFormat} does the right thing).
	 * </p>
	 */
	public void setDecimalAllowed(boolean decimalAllowed) {
		attributes.setDecimalAllowed(decimalAllowed);
		if (!decimalAllowed) {
			setMinimumFractionDigits(0);
		}
		requestRepaint();
	}

	/**
	 * See {@link NumberFieldAttributes#isDecimalAllowed()}.
	 */
	public boolean isDecimalAllowed() {
		return attributes.isDecimalAllowed();
	}

	/**
	 * @param text
	 *            The error text to display in case of an invalid field value.<br/>
	 *            Caution: If the argument is "" or <code>null</code>, the field
	 *            won't be recognizable as invalid!
	 */
	public void setErrorText(String text) {
		this.errorText = text;
	}

	/**
	 * @return The error text displayed in case of an invalid field value.
	 */
	public String getErrorText() {
		return errorText;
	}

	/**
	 * See {@link NumberFieldAttributes#isGroupingUsed()}.
	 */
	public boolean isGroupingUsed() {
		return attributes.isGroupingUsed();
	}

	/**
	 * See {@link NumberFieldAttributes#setGroupingUsed(boolean)}.
	 */
	public void setGroupingUsed(boolean group) {
		attributes.setGroupingUsed(group);
		requestRepaint();
	}

	/**
	 * See {@link DecimalFormat#getGroupingSize()}.
	 */
	public int getGroupingSize() {
		return groupingSize;
	}

	/**
	 * See {@link DecimalFormat#setGroupingSize(int)}.
	 */
	public void setGroupingSize(int groupingSize) {
		this.groupingSize = groupingSize;
		requestRepaint();
	}

	/**
	 * See {@link DecimalFormat#getMinimumFractionDigits()}.
	 */
	public int getMinimumFractionDigits() {
		return minimumFractionDigits;
	}

	/**
	 * See {@link DecimalFormat#setMinimumFractionDigits(int)}.
	 */
	public void setMinimumFractionDigits(int minimumDigits) {
		minimumFractionDigits = minimumDigits;
		requestRepaint();
	}

	/**
	 * See {@link DecimalFormat#isDecimalSeparatorAlwaysShown()}.
	 */
	public boolean isDecimalSeparatorAlwaysShown() {
		return decimalSeparatorAlwaysShown;
	}

	/**
	 * See {@link DecimalFormat#setDecimalSeparatorAlwaysShown(boolean)}.
	 */
	public void setDecimalSeparatorAlwaysShown(boolean showAlways) {
		decimalSeparatorAlwaysShown = showAlways;
		requestRepaint();
	}

	/**
	 * See {@link NumberFieldAttributes#getDecimalPrecision()}.
	 */
	public int getDecimalPrecision() {
		return attributes.getDecimalPrecision();
	}

	/**
	 * See {@link NumberFieldAttributes#setDecimalPrecision(int)}.
	 */
	public void setDecimalPrecision(int maximumDigits) {
		attributes.setDecimalPrecision(maximumDigits);
		requestRepaint();
	}

	/**
	 * See {@link NumberFieldAttributes#getDecimalSeparator()}.
	 */
	public char getDecimalSeparator() {
		return attributes.getDecimalSeparator();
	}

	/**
	 * See {@link NumberFieldAttributes#getEscapedDecimalSeparator()}.
	 */
	public String getEscapedDecimalSeparator() {
		return attributes.getEscapedDecimalSeparator();
	}

	/**
	 * Sets the decimal separator. If the field has a value containing the old
	 * decimal separator, it is replaced with the new one.
	 */
	public void setDecimalSeparator(char newSeparator) {
		replaceSeparatorInField(getDecimalSeparator(), newSeparator);
		attributes.setDecimalSeparator(newSeparator);
		requestRepaint();
	}

	private void replaceSeparatorInField(char oldSeparator, char newSeparator) {
		String value = (String) getValue();
		if (value != null) {
			String valueWithReplacedSeparator = value.replace(oldSeparator,
					newSeparator);
			setValue(valueWithReplacedSeparator);
		}
	}

	/**
	 * See {@link NumberFieldAttributes#getGroupingSeparator()}.
	 */
	public char getGroupingSeparator() {
		return attributes.getGroupingSeparator();
	}

	/**
	 * See {@link NumberFieldAttributes#getEscapedGroupingSeparator()}.
	 */
	public String getEscapedGroupingSeparator() {
		return attributes.getEscapedGroupingSeparator();
	}

	/**
	 * Sets the grouping separator. If the field has a value containing the old
	 * grouping separator, it is replaced with the new one.
	 */
	public void setGroupingSeparator(char newSeparator) {
		replaceSeparatorInField(getGroupingSeparator(), newSeparator);
		attributes.setGroupingSeparator(newSeparator);
		requestRepaint();
	}

	/**
	 * See {@link NumberFieldAttributes#getMinValue()}.
	 */
	public double getMinValue() {
		return attributes.getMinValue();
	}

	/**
	 * See {@link NumberFieldAttributes#setMinValue(double)}.
	 */
	public void setMinValue(double minValue) {
		attributes.setMinValue(minValue);
		requestRepaint();
	}

	/**
	 * See {@link NumberFieldAttributes#getMaxValue()}.
	 */
	public double getMaxValue() {
		return attributes.getMaxValue();
	}

	/**
	 * See {@link NumberFieldAttributes#setMaxValue(double)}.
	 */
	public void setMaxValue(double maxValue) {
		attributes.setMaxValue(maxValue);
		requestRepaint();
	}

	/**
	 * See {@link NumberFieldAttributes#isNegativeAllowed()}.
	 */
	public boolean isNegativeAllowed() {
		return attributes.isNegativeAllowed();
	}

	/**
	 * See {@link NumberFieldAttributes#setNegativeAllowed(boolean)}.
	 */
	public void setNegativeAllowed(boolean negativeAllowed) {
		attributes.setNegativeAllowed(negativeAllowed);
		requestRepaint();
	}

	/**
	 * @return The field's value as a double value. If the field contains no
	 *         parsable number, 0.0 is returned.
	 */
	public double getDoubleValueDoNotThrow() {
		try {
			return Double.valueOf(getValueNonLocalized());
		} catch (Exception e) {
			return 0.0;
		}
	}

	/**
	 * @return The field's value as a string representation of a decimal number
	 *         with '.' as decimal separator and cutted grouping separators.
	 *         Example: If the field's value is "2.546,99", this method will
	 *         return "2546.99". If the field is empty, "0" is returned.
	 */
	public String getValueNonLocalized() {
		String value = (String) getValue();
		if (value == null || "".equals(value)) {
			return "0";
		}
		String groupingSeparator = String.valueOf(getGroupingSeparator());
		return value.replace(groupingSeparator, "").replace(
				getDecimalSeparator(), '.');
	}

	/**
	 * If the given value is the string representation of a decimal number with
	 * '.' as decimal separator (e.g. 12345.67), this method will return the
	 * value with replaced decimal seperator as it was set with
	 * {@link #setDecimalSeparator(char)}.
	 */
	public String replacePointWithDecimalSeparator(String value) {
		return value.replace('.', getDecimalSeparator());
	}

}

